package com.rhaggarty.blackjack.game;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;

import com.rhaggarty.data.type.game.Card;
import com.rhaggarty.data.type.game.Ordinal;

/**
 * A representation of a Blackjack Hand.  
 * 
 * A hand will contain all cards dealt for the life of the hand, 
 * but will only return values that are within the bounds of a valid blackjack hand.  For example, 
 * Ac-10c-6s will return only 17 as the possible values, as the alternate 27 is outside the bounds of a 
 * valid hand.
 * 
 * @author Ryan Haggarty (ryanmh@gmail.com) 
 */
public final class Hand {

    public static final int BLACKJACK = 21;
    
    private static final int MAX_VALUE = BLACKJACK;

    private final Set<Card> cards = new LinkedHashSet<Card>();
    
    private volatile SortedSet<Integer> allValues = new TreeSet<Integer>();

    public Hand(final Card firstCard) {
        addCard(firstCard);
    }

    public void addCard(final Card card) {
        Validate.notNull(card, "Card cannot be null");
        
        cards.add(card);
        populateValues();
    }

    public boolean contains(final Ordinal ordinal) {
        for (final Card card : cards) {
            if (card.getOrdinal().equals(ordinal)) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Returns a sorted (ascending) set of all possible values for the current hand.  This set will
     * only contain values within the bounds of a valid poker hand. In the case that a hand has not yet 
     * been dealt or there are no valid values > 0 && <= 21, an empty set will be returned.
     */
    public SortedSet<Integer> getAllValues() {
        if (allValues.isEmpty()) {
            populateValues();
        } 
        
        return Collections.unmodifiableSortedSet(allValues);
    }
    
    public Set<Card> getCards() {
        return Collections.unmodifiableSet(cards);
    }
    
    /**
     * Returns a formatted representation of the possible values for the current hand.  This will be an ' or ' 
     * delimited list of all possible values.
     */
    public String getFormattedValues() {
        if (allValues.isEmpty()) {
            return StringUtils.EMPTY;
        }
        
        return StringUtils.join(allValues, " or ");
    }
    
    /**
     * Returns the highest valid value the current hand represents.  If there are no valid hands, this returns 0.
     */
    public int getHigh() {
        if (!allValues.isEmpty()) {
            return allValues.last();
        } else {
            return 0;
        }
    }

    public boolean hasBlackjack() {
        return cards.size() == 2 && getHigh() == 21;
    }
    
    private void populateValues() {
        final List<int[]> values = new ArrayList<int[]>(cards.size());
        for (final Card card : cards) {
            values.add(card.getOrdinal().getValues());
        }
        
        allValues.clear();
        allValues.addAll(populateAll(values, 0));
    }
    
    private static SortedSet<Integer> populateAll(final List<int[]> values, final int total) {
        final SortedSet<Integer> totals = new TreeSet<Integer>();
        
        if (values.size() > 0) {
            final int[] next = values.get(0);
            for (int i = 0; i < next.length; i++) {
                totals.addAll(populateAll(values.subList(1, values.size()), total + next[i]));
            }            
        } else if (total <= MAX_VALUE){
            totals.add(total);
        }
        
        return totals;
    }
    
    @Override
    public String toString() {
        return StringUtils.join(cards, ", ");
    }
}
